﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

using BizElements.Core;
using BizElements.TemplateEngine;

namespace BizElements.Generator
{    
    sealed class DbProjectGeneratorProperties : ICodeGeneratorProperties
    {
        #region ICodeGeneratorProperties Properties.

        public string[] SelectedItems { get; set; }
        public ITemplate[] ItemTemplates { get; set; }
        public string OutputFolder { get; set; }
        public IDictionary<string, IDictionary<string, string>> AllComponentVariables { get; set; }

        #endregion

        #region DB specific properties.

        public IMetadataSource MetaDataSource { get; set; }        
        public Regex ExcludedColumnsRegex { get; set; }
        public string[] SelectedProcedures { get; set; }        
        public ITemplate SpAndCatalogTemplate { get; set; }
        public ITypeMapper TypeMapper { get; set; }
        public string[] SelectedViews { get; set; }        

        #endregion

        #region Validate.

        public string[] Validate()
        {
            List<string> errors = new List<string>();
            if (this.MetaDataSource == null)
                errors.Add(Messages.GeneratorCtl_MetadataSourceMustBeConfiguredAndInitalized);

            if (this.SelectedItems == null || this.SelectedItems.Length == 0)
                errors.Add(Messages.GenaratorCtl_ListOfSelectedTablesIsNull);

            if (this.ItemTemplates == null || this.ItemTemplates.Length == 0)
                errors.Add(Messages.GeneratorCtl_TableItemTemplates_AtLeastOneIsRequired);

            if (this.SpAndCatalogTemplate == null)
                errors.Add(Messages.GeneratorCtl_StoredProcedureAndCatalogClassesTemplateMustBeSelectedAndConfigured);
            
            if (string.IsNullOrEmpty(OutputFolder))
                errors.Add(Messages.GeneratorCtl_OutputFolderMustBeDefined);

            if (this.TypeMapper == null)
                errors.Add(Messages.GeneratorCtl_TypeTemplateMustSelected);

            return errors.ToArray();
        }

        #endregion
    }       

    sealed class DbProjectGenerator : ICodeGenerator
    {
        const int TableMetaCost = 70;
        const int SpMetaCost = 10;
        static readonly int MetaCost = TableMetaCost + SpMetaCost;
        const int EntityClassesCost = 16;
        const int OtherCost = 4;

        #region Fields.

        bool stopGenerator;
        IMetadataSource metadataSource;
        string outputFolder;
        string[] selectedTables;
        Regex excludedColumnsCriteria;
        string[] selectedProcedures;
        ITypeMapper typeMapper;
        DefaultNamingConvention classItemTemplate = new DefaultNamingConvention();
        ITemplate spAndCatalogTemplate;
        ITemplate[] tableItemTemplates;
        SortedList<string, string> selectedViews;

        #endregion

        #region CTor.

        public DbProjectGenerator(DbProjectGeneratorProperties properties)
        {
            this.metadataSource = properties.MetaDataSource;
            this.outputFolder = TextUtil.EnsureSuffix(properties.OutputFolder, @"\");
            this.selectedTables = properties.SelectedItems;
            this.excludedColumnsCriteria = properties.ExcludedColumnsRegex;
            this.selectedProcedures = properties.SelectedProcedures ?? new string[0];
            this.typeMapper = properties.TypeMapper;
            this.spAndCatalogTemplate = properties.SpAndCatalogTemplate;
            this.tableItemTemplates = properties.ItemTemplates;
            this.selectedViews = new SortedList<string, string>(properties.SelectedViews.Length);
            foreach (string view in properties.SelectedViews)
                this.selectedViews.Add(view, view);

            this.spAndCatalogTemplate.AllComponentVariables = properties.AllComponentVariables;
            foreach (ITemplate template in this.tableItemTemplates)
                template.AllComponentVariables = properties.AllComponentVariables;
            
        }

        #endregion

        #region Events.

        public event EventHandler<EventArgs<GenerationTraceItem>> ItemGenerated;

        private void FireItemGenerated(string message, int progresPercentage)
        {
            if (this.ItemGenerated != null)
                this.ItemGenerated(this, new EventArgs<GenerationTraceItem>(new GenerationTraceItem(message ?? "", progresPercentage)));
        }

        public event EventHandler<EventArgs<GenerationTraceItem>> GenerationCompleted;

        private void FireGenerationCompleted()
        {
            if (this.GenerationCompleted != null)
                this.GenerationCompleted(this, new EventArgs<GenerationTraceItem>(new GenerationTraceItem("", 100)));
        }

        #endregion

        public void StopGenerator()
        {
            // This can be called from another thread.
            this.stopGenerator = true;
        }

        #region GenerateCode.

        public void GenerateCode()
        {
            try
            {
                GenerateAllCode();
            }
            finally
            {
                FireGenerationCompleted();
            }
        }

        private void GenerateAllCode()
        {
            this.stopGenerator = false;            
            FireItemGenerated(Messages.DbProjectGenerator_CodeGenerationStarted, 0);
            DateTime start = DateTime.Now;
            CreateOutputFolders();
            
            OpenConnectionToMetaDataSource();
            Table[] tables = RetrieveTableMetaData();
            if (tables == null)
                return;
            StoredProcedure[] procedures = RetrieveSpMetaData();
            if (procedures == null)
                return;
            CloseConnectionToMetaDataSource();

            OptimizeMetaByIndexing(tables);

            bool multiFileTemplatesOk = GenerateCodeForSelectedTables(tables);
            if (!multiFileTemplatesOk)
                return;

            bool singeFileTemplatesOk = GenerateSingleFileTemplateForTables(tables);
            if (!singeFileTemplatesOk)
                return;

            StoredProcedureWrapperClass spWrapper = new StoredProcedureWrapperClass() { TypeMapper = this.typeMapper, Procedures = procedures };
            GenerateSpWrappers(spWrapper, MetaCost + EntityClassesCost);

            string msgTotalStats = string.Format(Messages.DbProjectGenerator_CompletedTimeUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgTotalStats , 100);
        }        

        private void CreateOutputFolders()
        {
            CreateDirectory(this.outputFolder);

            string spAndCatDir = GenUtil.GetOutputSubdirectoryVariable(this.spAndCatalogTemplate);
            if (!string.IsNullOrEmpty(spAndCatDir))
                CreateDirectory(this.outputFolder + spAndCatDir);

            foreach (ITemplate tblItemTemplate in this.tableItemTemplates)
            {
                string subdir = GenUtil.GetOutputSubdirectoryVariable(tblItemTemplate);
                if (!string.IsNullOrEmpty(subdir))
                    CreateDirectory(this.outputFolder + subdir);
            }
        }

        private void CreateDirectory(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
                string msg = string.Format(Messages.DbProjectGenerator_DirectoryXCreated, path);
                FireItemGenerated(msg, 0);
            }
        }

        #endregion

        #region Table and SP meta - fetch from medadata source.

        private Table[] RetrieveTableMetaData()
        {
            ICollection<string> allGeneratedTableNames = new HashSet<string>(this.selectedTables);
            
            DateTime start = DateTime.Now;
            this.metadataSource.ExcludedColumnsCriteria = this.excludedColumnsCriteria;
            Table[] tables = new Table[this.selectedTables.Length];
            int percentage = 0;            
            for (int i = 0; i < this.selectedTables.Length; i++)
            {
                percentage = (int)(TableMetaCost * i / tables.Length);
                if (this.stopGenerator)
                {
                    this.metadataSource.CloseConnection();
                    FireItemGenerated(Messages.DbProjectGenerator_GeneratorStopped, percentage);
                    return null;
                }

                string currTable = this.selectedTables[i];

                tables[i] = this.metadataSource.FetchTableMetadata(currTable, allGeneratedTableNames, this.classItemTemplate, this.typeMapper);
                tables[i].IsView = this.selectedViews.ContainsKey(currTable);
                string msg = string.Format(Messages.DbProjectGenerator_RetrievedMetaForTableX, currTable);
                FireItemGenerated(msg, percentage);
            }

            string msgStatistics = string.Format(Messages.DbProjectGenerator_RetrievedMetaForAllTablesTimeUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgStatistics, percentage);
            return tables;
        }

        private StoredProcedure[] RetrieveSpMetaData()
        {
            DateTime start = DateTime.Now;
            StoredProcedure[] procedures = new StoredProcedure[this.selectedProcedures.Length];
            int percentage = 0;
            for (int i = 0; i < this.selectedProcedures.Length; i++)
            {
                percentage = (int)(SpMetaCost * i / procedures.Length + TableMetaCost);
                if (this.stopGenerator)
                {
                    this.metadataSource.CloseConnection();
                    FireItemGenerated(Messages.DbProjectGenerator_GeneratorStopped, percentage);
                    return null;
                }

                string currProcedure = (string)this.selectedProcedures[i];
                procedures[i] = this.metadataSource.FetchStoredProcedureMetaData(currProcedure);
                //procedures[i].PrefixesToBeStripped = this.spPrefixesToBeStripped;
                string msg = string.Format(Messages.DbProjectGenerator_RetrievedMetaForProcedureX, currProcedure);
                FireItemGenerated(msg, percentage);
            }

            string msgStatistics = string.Format(Messages.DbProjectGenerator_RetrievedMetaForAllProcsTimeUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgStatistics, percentage);
            return procedures;
        }

        private void OpenConnectionToMetaDataSource()
        {
            FireItemGenerated(Messages.DbProjectGenerator_OpenningConnToMetadata, 0);
            this.metadataSource.OpenConnection();
            string serverandCatalogMsg = string.Format(Messages.DbProjectGenerator_ServerXCatalogY, this.metadataSource.Server, this.metadataSource.Catalog);
            FireItemGenerated(serverandCatalogMsg, 0);
        }

        private void CloseConnectionToMetaDataSource()
        {
            this.metadataSource.CloseConnection();
            FireItemGenerated(Messages.DbProjectGenerator_ConnectionClosed, MetaCost);
        }

        #endregion

        #region Temp table map and indices.

        // Temporary map used by template to optimize code generation.
        Dictionary<string, int> generatedTablesIndices;

        private void OptimizeMetaByIndexing(Table[] tables)
        {
            // Table-map speeds up code generation by reducing loops while generating code for ORM components.
            CreateGeneratedTablesIndices(tables);
            foreach (Table tbl in tables)
            {
                // Tables need to know for which tables the code is generated. Required when generating foreign table members.
                tbl.GeneratedTables = tables;
                tbl.IsTableGeneratedAlgorithm = this.IsTableGenerated;
                tbl.IndexOfGeneratedTableAlgorithm = this.IndexOfGeneratedTable;
            }
        }        

        private void CreateGeneratedTablesIndices(Table[] tables)
        {
            this.generatedTablesIndices = new Dictionary<string, int>();
            for (int i = 0; i < tables.Length; i++)
            {
                Table tbl = tables[i];
                this.generatedTablesIndices[tbl.TableName] = i;
            }
        }

        private bool IsTableGenerated(string tableName)
        {
            return this.generatedTablesIndices.ContainsKey(tableName);
        }

        private int IndexOfGeneratedTable(string tableName)
        {
            int index;
            if (this.generatedTablesIndices.TryGetValue(tableName, out index))
                return index;
            else
                return -1;
        }

        #endregion

        #region Generate code for selected tables: entity, meta and collection classes.

        private bool GenerateCodeForSelectedTables(Table[] tables)
        {
            List<ITemplate> multiFileTemplates = GenUtil.GetMultiFileTemplates(this.tableItemTemplates);
            if (multiFileTemplates.Count == 0)
                return true;                                

            DateTime start = DateTime.Now;
            int percentage = MetaCost;
            for (int i = 0; i < tables.Length; i++)
            {
                percentage = (int)(MetaCost + EntityClassesCost * i / tables.Length);
                if (this.stopGenerator)
                {
                    FireItemGenerated(Messages.DbProjectGenerator_GeneratorStopped, percentage);
                    return false;
                }

                GenerateCodeForTable(tables[i], multiFileTemplates);
                string msg = string.Format(Messages.DbProjectGenerator_GeneratedCodeForTableX, tables[i].TableName);
                FireItemGenerated(msg, percentage);
            }

            string msgStatistics = string.Format(Messages.DbProjectGenerator_GeneratedCodeForAllTablesTimesUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgStatistics, percentage);
            return true;
        }

        private void GenerateCodeForTable(Table tbl, List<ITemplate> multiFileTemplates)
        {
            foreach (ITemplate template in multiFileTemplates)
            {
                if (template.IsValidInputComponent(tbl))
                {
                    string classFileName = GenUtil.CreateClassOutputFilePath(this.classItemTemplate.GenerateClassName(tbl.TableName), tbl.GetType(), template, this.outputFolder);
                    using (StreamWriter classOutput = new StreamWriter(classFileName, /*overwrite existing file*/ false, Encoding.UTF8))
                    {
                        template.Run(classOutput, tbl);
                    }
                }
            }
        }              

        #endregion

        #region GenerateSingleFileTemplatesForObjects - template generates single file that contains code for all objects/items.

        private bool GenerateSingleFileTemplateForTables(Table[] tables)
        {
            List<ITemplate> singleFileTemplates = GenUtil.GetSingleFileTemplates(this.tableItemTemplates);
            if (singleFileTemplates.Count == 0)
                return true;

            int percentage = 100 - OtherCost; 
            foreach (ITemplate template in singleFileTemplates)
            {
                if (this.stopGenerator)
                {
                    FireItemGenerated(Messages.DotNetProjectGenerator_GeneratorStopped, percentage);
                    return false;
                }

                string generatedFileName = GenerateSingleFileForTemplate(template, tables);
                string msg = string.Format(Messages.DotNetProjectGenerator_GeneratedFileX, generatedFileName);
                FireItemGenerated(msg, percentage);
            }

            return true;
        }

        private string GenerateSingleFileForTemplate(ITemplate template, Table[] tables)
        {
            string fileNameWithExt = GenUtil.GetSingleFieldVariableOrSetting(template);
            string filePath = GenUtil.CreateSingleFilePathForTemplate(template, this.outputFolder, fileNameWithExt);
            using (StreamWriter codeOutput = new StreamWriter(filePath, /*overwrite existing file*/ false, Encoding.UTF8))
            {
                template.Run(codeOutput, tables);
            }

            return fileNameWithExt;
        }

        #endregion

        #region SP class.

        private void GenerateSpWrappers(StoredProcedureWrapperClass spWrapper, int percentageDoneBeforeSpWrappers)
        {
            DateTime startSP = DateTime.Now;            
            string spFile = CreateSpOutputFilePath();
            using (StreamWriter sw = new StreamWriter(spFile, /*overwrite existing file*/ false, Encoding.UTF8))
            {
                this.spAndCatalogTemplate.Run(sw, spWrapper);
            }

            string msgStatistics = string.Format(Messages.DbProjectGenerator_GeneratedCatalogAndSpClassTimeUsedX, GenUtil.GetDuration(startSP));
            FireItemGenerated(msgStatistics, 100 - OtherCost);
        }

        private string CreateSpOutputFilePath()
        {
            string catalogFile;
            string subdir = GenUtil.GetOutputSubdirectoryVariable(this.spAndCatalogTemplate);
            string specifiedFile = GenUtil.GetSingleFieldVariableOrSetting(this.spAndCatalogTemplate);
            if (string.IsNullOrEmpty(specifiedFile))
            {
                string fileName = this.spAndCatalogTemplate.GetVariable("ProjectNamespace") ?? "SpAndCatalog";
                string nameSuffix = this.spAndCatalogTemplate.GetVariable("FileNameSuffix");
                string extension = this.spAndCatalogTemplate.GetSetting("FileExtension") ?? "cs";
                
                if (string.IsNullOrEmpty(subdir))
                    catalogFile = string.Format("{0}{1}{2}.{3}", this.outputFolder, fileName, nameSuffix, extension);
                else
                    catalogFile = string.Format("{0}{1}\\{2}{3}.{4}", this.outputFolder, subdir, fileName, nameSuffix, extension);
            }
            else
            {
                if (string.IsNullOrEmpty(subdir))
                    catalogFile = string.Format("{0}{1}", this.outputFolder, specifiedFile);
                else
                    catalogFile = string.Format("{0}{1}\\{2}", this.outputFolder, subdir, specifiedFile);
            }            

            return catalogFile;
        }

        #endregion
    }    
}
